﻿using System;
using System.Data.SqlClient;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Management.Automation.Provider;
using System.Text.RegularExpressions;
using ReportService2005;

namespace SSRSProvider
{
    /// <summary>
    /// PowerShell provider class for Reporting Services web service.
    /// </summary>
    [CmdletProvider( "ReportingServices", ProviderCapabilities.None )]
    public class ReportingServicesProvider : NavigationCmdletProvider
    {
        private readonly string m_PathSeparator = "\\";
        private readonly string m_RSPathSeparator = "/";

        /// <summary>
        /// Test to see if the specified path is syntactically valid.
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        protected override bool IsValidPath( string path )
        {
            if ( string.IsNullOrEmpty( path ) )
            {
                return false;
            }

            if ( IsPathDrive( path ) )
            {
                return true;
            }

            path = path.Replace( m_PathSeparator, m_RSPathSeparator );
            string[] thePathParts = path.Split( m_RSPathSeparator.ToCharArray() );
            foreach ( string thePathPart in thePathParts )
            {
                if ( thePathPart.Length == 0 )
                {
                    return false;
                }
            }
            return true;
        }

        /// <summary>
        /// Returns true if the requested item exists on the report server.
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        protected override bool ItemExists( string path )
        {
            if ( IsPathDrive( path ) )
            {
                return true;
            }

            // empty or null path gets converted to root
            if ( string.IsNullOrEmpty( path ) )
            {
                path = m_PathSeparator;
            }

            // Remove the drive portion of path and get just the name of the item and check for existance
            //path = path.Replace( PSDriveInfo.Root, string.Empty );
            bool theItemExists = GetRSItem( path ) != null;
            return theItemExists;
        }

        /// <summary>
        /// Gets a single item from RS and writes it out
        /// </summary>
        /// <param name="path"></param>
        protected override void GetItem( string path )
        {
            CatalogItem theOutputItem = GetRSItem( path );
            if ( theOutputItem != null )
            {
                WriteItemObject( theOutputItem, theOutputItem.Path, theOutputItem.Type == ItemTypeEnum.Folder );
            }
            else
            {
                WriteError( new ErrorRecord( new ArgumentException(), "NotFound", ErrorCategory.ObjectNotFound, path ) );
            }
        }

        /// <summary>
        /// Gets a list of the child items of the given path
        /// </summary>
        /// <param name="path"></param>
        /// <param name="recurse"></param>
        protected override void GetChildItems( string path, bool recurse )
        {
            CatalogItem[] theItems = GetRSChildItems( path );
            if ( theItems != null && theItems.Length > 0 )
            {
                foreach ( CatalogItem theItem in theItems )
                {
                    WriteItemObject( theItem, theItem.Path, true );

                    if ( recurse && theItem.Type == ItemTypeEnum.Folder )
                    {
                        GetChildItems( theItem.Path, recurse );
                    }
                }
            }
        }

        /// <summary>
        /// Gets a list of the name of the child items of the given path
        /// </summary>
        /// <param name="path"></param>
        /// <param name="returnContainers"></param>
        protected override void GetChildNames( string path, ReturnContainers returnContainers )
        {
            CatalogItem[] theItems = GetRSChildItems( path );
            if ( theItems != null && theItems.Length > 0 )
            {
                foreach ( CatalogItem theItem in theItems )
                {
                    if ( theItem.Type == ItemTypeEnum.Folder )
                    {
                        GetChildNames( theItem.Path, returnContainers );

                        if ( returnContainers == ReturnContainers.ReturnAllContainers )
                        {
                            WriteItemObject( theItem.Name, theItem.Path, true );
                        }
                        else
                        {
                            // TODO: Only write when it matches the filters 
                        }
                    }
                    else
                    {
                        WriteItemObject( theItem.Name, theItem.Path, false );
                    }
                }
            }
        }

        /// <summary>
        /// Checks to see if the path has children below it
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        protected override bool HasChildItems( string path )
        {
            bool theHasChildren = false;
            CatalogItem[] theItems = GetRSChildItems( path );
            if ( theItems != null && theItems.Length > 0 )
            {
                theHasChildren = true;
            }
            return theHasChildren;
        }

        /// <summary>
        /// Makes a path in a Reporting Services format, given a parent/child combo
        /// </summary>
        /// <param name="parent"></param>
        /// <param name="child"></param>
        /// <returns></returns>
        protected override string MakePath( string parent, string child )
        {
            string thePath = base.MakePath( parent, child );
            return thePath;
        }

        /// <summary>
        /// Gets the parent portion of the path
        /// </summary>
        /// <param name="path"></param>
        /// <param name="root"></param>
        /// <returns></returns>
        protected override string GetParentPath( string path, string root )
        {
            string theParentPath = base.GetParentPath( path, root );
            return theParentPath;
        }

        /// <summary>
        /// Gets the item/child portion of the path
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        protected override string GetChildName( string path )
        {
			path = path.Replace( m_RSPathSeparator, m_PathSeparator );
			path = path.TrimEnd( m_PathSeparator.ToCharArray() );
			string theChildName = null;
			int theSepIndex = path.LastIndexOf( m_PathSeparator );
			if ( theSepIndex == -1 )
			{
				theChildName = path;
			}
			else
			{
				theChildName = path.Substring( theSepIndex + 1 );
			}
			return theChildName;
        }

        /// <summary>
        /// Returns true if the item is a container
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        protected override bool IsItemContainer( string path )
        {
            if ( IsPathDrive( path ) )
            {
                return true;
            }

            bool theIsContainer = false;
            CatalogItem theItem = GetRSItem( path );
            if ( theItem != null )
            {
                theIsContainer = theItem.Type == ItemTypeEnum.Folder;
            }
            return theIsContainer;
        }

        /// <summary>
        /// Creates a new drive to talk with a Reporting Services instance
        /// </summary>
        /// <param name="drive"></param>
        /// <returns></returns>
        protected override PSDriveInfo NewDrive( PSDriveInfo drive )
        {
            // null drive kills us
            if ( drive == null )
            {
                WriteError( new ErrorRecord( new ArgumentException( "drive" ), "NullDrive", ErrorCategory.InvalidArgument, null ) );
                return null;
            }

            // No root is not allowed
            if ( string.IsNullOrEmpty( drive.Root ) )
            {
                WriteError( new ErrorRecord( new ArgumentException( "drive.Root" ), "NoRoot", ErrorCategory.InvalidArgument, drive ) );
                return null;
            }

            // If this is already a RS drive, return it
            if ( drive is ReportingServicesDriveInfo )
            {
                return drive;
            }

            // Create the drive, passing in the dynamic parameters
            ReportingServicesDriveParameters theParams = DynamicParameters as ReportingServicesDriveParameters;
            ReportingServicesDriveInfo theRSDrive = new ReportingServicesDriveInfo( drive, theParams );
            return theRSDrive;
        }

        /// <summary>
        /// Returns the dynamics paramters for the reporting services drive about to be added
        /// </summary>
        /// <returns></returns>
        protected override object NewDriveDynamicParameters()
        {
            return new ReportingServicesDriveParameters();
        }

        /// <summary>
        /// Sets up a default drive for localhost (url not verified)
        /// </summary>
        /// <returns></returns>
        protected override System.Collections.ObjectModel.Collection<PSDriveInfo> InitializeDefaultDrives()
        {
            PSDriveInfo theDefaultRSDrive = new PSDriveInfo( "rslocal", ProviderInfo, m_PathSeparator, "Local Reporting Services", null );
            ReportingServicesDriveParameters theParams = new ReportingServicesDriveParameters()
            {
                WebServiceUrl = "http://localhost:80/ReportServer/ReportService2005.asmx"
            };
            return new System.Collections.ObjectModel.Collection<PSDriveInfo>(
                new PSDriveInfo[] { new ReportingServicesDriveInfo( theDefaultRSDrive, theParams ) } );
        }

        /// <summary>
        /// Creates a new reporting services item.
        /// </summary>
        /// <param name="path"></param>
        /// <param name="itemTypeName"></param>
        /// <param name="newItemValue"></param>
        protected override void NewItem( string path, string itemTypeName, object newItemValue )
        {
            switch ( itemTypeName.ToLower() )
            {
                case "directory":
                case "folder":
                    NewItemFolder( path, itemTypeName, newItemValue );
                    break;
                case "datasource":
                    NewItemDataSource( path );
                    break;
                case "report":
                    NewItemReport( path, newItemValue );
                    break;
                case "resource":
                    NewItemResource( path, newItemValue );
                    break;
                default:
                    WriteError( new ErrorRecord( new ArgumentException( "Unknown Type" ), "InvalidArgument", ErrorCategory.InvalidArgument, itemTypeName ) );
                    break;
            }
        }

        /// <summary>
        /// Create a new resource on report server.
        /// </summary>
        /// <param name="path"></param>
        /// <param name="newItemValue"></param>
        private void NewItemResource( string path, object newItemValue )
        {
            // Ensure the source file exists
            string theSourcePath = newItemValue as string;
            if ( !File.Exists( theSourcePath ) )
            {
                WriteError( new ErrorRecord( new FileNotFoundException(), "FileNotFound", ErrorCategory.ObjectNotFound, newItemValue ) );
                return;
            }

            string theName = Path.GetFileNameWithoutExtension( theSourcePath );

            WriteVerbose( string.Format( "Creating resource {0}\\{1}", path, theName ) );

            try
            {
                // Get mime type of resource from parameter
                string theMimeType = "none";
                ResourceParameters theParams = DynamicParameters as ResourceParameters;
                if ( theParams != null )
                {
                    theMimeType = theParams.MimeType;
                }

                // Read source file (rdl) into byte stream and create a report
                using ( FileStream theStream = File.OpenRead( theSourcePath ) )
                {
                    Byte[] theDefinition = new Byte[theStream.Length];
                    theStream.Read( theDefinition, 0, (int)theStream.Length );
                    ReportingServicesDriveInfo theDrive = PSDriveInfo as ReportingServicesDriveInfo;
                    theDrive.RSProxy.CreateResource( theName, NormalizePath( path ), Force, theDefinition, theMimeType, null );
                }
            }
            catch ( System.Web.Services.Protocols.SoapException inEx )
            {
                if ( "rsItemNotFound" == inEx.Detail["ErrorCode"].InnerXml )
                {
                    WriteError( new ErrorRecord( new ArgumentException( string.Format( "Folder {0} not found", path ) ), "FolderExists", ErrorCategory.InvalidArgument, path ) );
                }
                else
                {
                    WriteError( new ErrorRecord( inEx, "SoapException", ErrorCategory.InvalidData, path ) );
                }
            }
        }

        /// <summary>
        /// Creates a new data source using dynamic parameters.
        /// </summary>
        /// <param name="path"></param>
        private void NewItemDataSource( string path )
        {
            DataSourceParameters theParams = DynamicParameters as DataSourceParameters;
            if ( theParams == null )
            {
                WriteError( new ErrorRecord( new ArgumentException( "Data source paramters not specified." ), "FolderExists", ErrorCategory.InvalidArgument, path ) );
                return;
            }

            // Build a connection string based off of the dynamic parameters
            SqlConnectionStringBuilder theConnStringBuilder = new SqlConnectionStringBuilder();
            theConnStringBuilder.DataSource = theParams.Server;
            theConnStringBuilder.InitialCatalog = theParams.Database;
            theConnStringBuilder.IntegratedSecurity = theParams.IntegratedAuthentication;

            // Create data source definition using the connection string
            DataSourceDefinition theDataSourceDef = new DataSourceDefinition();
            theDataSourceDef.ConnectString = theConnStringBuilder.ConnectionString;
            theDataSourceDef.CredentialRetrieval = CredentialRetrievalEnum.Integrated;
            theDataSourceDef.Enabled = true;
            theDataSourceDef.EnabledSpecified = true;
            theDataSourceDef.Extension = "SQL";
            theDataSourceDef.ImpersonateUser = false;
            theDataSourceDef.ImpersonateUserSpecified = false;
            theDataSourceDef.Prompt = null;
            theDataSourceDef.WindowsCredentials = false;

            try
            {
                string theParentPath = GetParentPath( path, null );
                string theDataSourceName = GetChildName( path );

                WriteVerbose( string.Format( "Creating data source {0}/{1}", theParentPath, theDataSourceName ) );

                ReportingServicesDriveInfo theDrive = PSDriveInfo as ReportingServicesDriveInfo;
                theDrive.RSProxy.CreateDataSource( theDataSourceName, NormalizePath( theParentPath ), Force, theDataSourceDef, null );
            }
            catch ( System.Web.Services.Protocols.SoapException inEx )
            {
                WriteError( new ErrorRecord( inEx, "SoapException", ErrorCategory.InvalidData, path ) );
            }
        }

        /// <summary>
        /// Creates a new report.
        /// </summary>
        /// <param name="path"></param>
        /// <param name="newItemValue"></param>
        private void NewItemReport( string path, object newItemValue )
        {
            // Ensure the source file exists
            string theSourcePath = newItemValue as string;
            if ( !File.Exists( theSourcePath ) )
            {
                WriteError( new ErrorRecord( new FileNotFoundException(), "FileNotFound", ErrorCategory.ObjectNotFound, newItemValue ) );
                return;
            }

            string theReportName = Path.GetFileNameWithoutExtension( theSourcePath );
            try
            {
                WriteVerbose( string.Format( "Creating report {0}/{1}", path, theReportName ) );

                // Read source file (rdl) into byte stream and create a report
                using ( FileStream theStream = File.OpenRead( theSourcePath ) )
                {
                    Byte[] theDefinition = new Byte[theStream.Length];
                    theStream.Read( theDefinition, 0, (int)theStream.Length );
                    ReportingServicesDriveInfo theDrive = PSDriveInfo as ReportingServicesDriveInfo;
                    Warning[] theWarnings = theDrive.RSProxy.CreateReport( theReportName, NormalizePath( path ), Force, theDefinition, null );

                    // Display any warnings
                    if ( theWarnings != null )
                    {
                        WriteVerbose( string.Format( "Warnings occurred when creating {0}/{1}", path, theReportName ) );
                        foreach ( Warning theCurrentWarning in theWarnings )
                        {
                            string theMsg = string.Format( "{0} {1} {2} - {3}",
                                theCurrentWarning.Code,
                                theCurrentWarning.ObjectType,
                                theCurrentWarning.ObjectName,
                                theCurrentWarning.Message );
                            WriteVerbose( theMsg );
                        }
                    }
                }

                // Update the report data source 
                UpdateReportDataSource( path, theReportName );
            }
            catch ( System.Web.Services.Protocols.SoapException inEx )
            {
                if ( "rsItemNotFound" == inEx.Detail["ErrorCode"].InnerXml )
                {
                    WriteError( new ErrorRecord( new ArgumentException( string.Format( "Folder {0} not found", path ) ), "FolderExists", ErrorCategory.InvalidArgument, path ) );
                }
                else
                {
                    WriteError( new ErrorRecord( inEx, "SoapException", ErrorCategory.InvalidData, path ) );
                }
            }
        }

        /// <summary>
        /// Updates the data sources on the report
        /// </summary>
        /// <param name="theParentReportPath"></param>
        /// <param name="theReportName"></param>
        private void UpdateReportDataSource( string inParentReportPath, string inReportName )
        {
            // See if we should set a shared data source on this report
            ReportParameters theParams = DynamicParameters as ReportParameters;
            if ( theParams != null && !string.IsNullOrEmpty( theParams.DataSourcePath ) )
            {
                try
                {
                    // Find all invalid data sources and update with the path from the parameter
                    ReportingServicesDriveInfo theDrive = PSDriveInfo as ReportingServicesDriveInfo;
                    DataSource[] theExistingDatasources = theDrive.RSProxy.GetItemDataSources( NormalizePath( inParentReportPath + m_PathSeparator + inReportName ) );
                    foreach ( DataSource theCurrentDataSource in theExistingDatasources )
                    {
                        if ( typeof( InvalidDataSourceReference ) == theCurrentDataSource.Item.GetType() )
                        {
                            DataSource theNewDataSource = new DataSource();
                            DataSourceReference theNewDataSourceRef = new DataSourceReference();
                            theNewDataSourceRef.Reference = theParams.DataSourcePath;
                            theNewDataSource.Item = theNewDataSourceRef;
                            theNewDataSource.Name = theParams.DataSourceName;
                            theDrive.RSProxy.SetItemDataSources( NormalizePath( inParentReportPath + m_PathSeparator + inReportName ), new DataSource[] { theNewDataSource } );
                        }
                    }
                }
                catch ( System.Web.Services.Protocols.SoapException inEx )
                {
                    WriteError( new ErrorRecord( inEx, "SoapException", ErrorCategory.InvalidData, theParams.DataSourcePath ) );
                }
            }
        }

        /// <summary>
        /// Creates a new folder/directory.
        /// </summary>
        /// <param name="path"></param>
        /// <param name="itemTypeName"></param>
        /// <param name="newItemValue"></param>
        private void NewItemFolder( string path, string itemTypeName, object newItemValue )
        {
            string theChild = GetChildName( path );
            string theParent = GetParentPath( path, null );
            if ( !ItemExists( theParent ) && !IsPathDrive( theParent ) && theParent != m_PathSeparator )
            {
                NewItem( theParent, itemTypeName, newItemValue );
            }
            try
            {
                WriteVerbose( string.Format( "Creating datasource {0}\\{1}", theParent, theChild ) );
                ReportingServicesDriveInfo theDrive = PSDriveInfo as ReportingServicesDriveInfo;
                theDrive.RSProxy.CreateFolder( theChild, NormalizePath( theParent ), null );
            }
            catch ( System.Web.Services.Protocols.SoapException inEx )
            {
                if ( "rsItemAlreadyExists" == inEx.Detail["ErrorCode"].InnerXml )
                {
                    WriteError( new ErrorRecord( new ArgumentException( string.Format( "Folder {0} already exists", path ) ), "FolderExists", ErrorCategory.InvalidArgument, path ) );
                }
                else
                {
                    WriteError( new ErrorRecord( inEx, "SoapException", ErrorCategory.InvalidData, path ) );
                }
            }
        }

        /// <summary>
        /// Serves up dynamic properties for the different types of items we support through New-Item
        /// </summary>
        /// <param name="path"></param>
        /// <param name="itemTypeName"></param>
        /// <param name="newItemValue"></param>
        /// <returns></returns>
        protected override object NewItemDynamicParameters( string path, string itemTypeName, object newItemValue )
        {
            object theReturn = null;
            switch ( itemTypeName.ToLower() )
            {
                case "report":
                    theReturn = new ReportParameters();
                    break;
                case "resource":
                    theReturn = new ResourceParameters();
                    break;
                case "datasource":
                    theReturn = new DataSourceParameters();
                    break;
                default:
                    theReturn = null;
                    break;
            }
            return theReturn;
        }

        /// <summary>
        /// Removes an item from the report server
        /// </summary>
        /// <param name="path"></param>
        /// <param name="recurse"></param>
        protected override void RemoveItem( string path, bool recurse )
        {
            if ( IsItemContainer( path ) )
            {
                CatalogItem[] theChildItems = GetRSChildItems( path );
                foreach ( CatalogItem theCurrentChildItem in theChildItems )
                {
                    RemoveItem( theCurrentChildItem.Path, recurse );
                }
            }

            // RS call to actually delete
            try
            {
                ReportingServicesDriveInfo theDrive = PSDriveInfo as ReportingServicesDriveInfo;
                theDrive.RSProxy.DeleteItem( NormalizePath( path ) );
            }
            catch ( System.Web.Services.Protocols.SoapException inEx )
            {
                WriteError( new ErrorRecord( inEx, "SoapException", ErrorCategory.InvalidData, path ) );
            }
        }

        /// <summary>
        /// Gets a RS item and returns it.
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        private CatalogItem[] GetRSChildItems( string path )
        {
            CatalogItem[] theReturnItems = null;
            ReportingServicesDriveInfo theDrive = PSDriveInfo as ReportingServicesDriveInfo;
            try
            {
                theReturnItems = theDrive.RSProxy.ListChildren( NormalizePath( path ), false );
            }
            catch ( System.Web.Services.Protocols.SoapException inEx )
            {
                WriteError( new ErrorRecord( inEx, "SoapException", ErrorCategory.InvalidData, path ) );
            }
            return theReturnItems;
        }

        /// <summary>
        /// Gets a single item
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        private CatalogItem GetRSItem( string path )
        {
            string theItemName = GetChildName( path );
            string theParentPath = GetParentPath( path, null );
            CatalogItem[] theRSItems = GetRSChildItems( theParentPath );
            CatalogItem theReturnItem = null;
            if ( theRSItems != null )
            {
                theReturnItem = ( from theItem in theRSItems
                                  where theItem.Name == theItemName
                                  select theItem ).FirstOrDefault();
            }
            return theReturnItem;
        }

        /// <summary>
        /// Tests to see if the path is a drive
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        private bool IsPathDrive( string path )
        {
            bool theIsDrive = false;

            // Ensure we have a drive
            if ( PSDriveInfo != null && !string.IsNullOrEmpty( PSDriveInfo.Root ) )
            {
                // Is the drive is valid in the path
                if ( string.IsNullOrEmpty( path.Replace( PSDriveInfo.Root, string.Empty ) ) ||
                    string.IsNullOrEmpty( path.Replace( PSDriveInfo.Root + m_PathSeparator, string.Empty ) ) )
                {
                    theIsDrive = true;
                }
            }
            return theIsDrive;
        }

        /// <summary>
        ///  Determines if the given path is rooted with the drive specification.
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        private bool IsPathIsRooted( string path )
        {
            bool theIsPathRooted = false;

            if ( PSDriveInfo != null &&
                !string.IsNullOrEmpty( PSDriveInfo.Root ) &&
                path.StartsWith( PSDriveInfo.Root, StringComparison.InvariantCultureIgnoreCase ) )
            {
                theIsPathRooted = true;
            }
            return theIsPathRooted;
        }

        /// <summary>
        /// Normailize the path (possibly includes a url)
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        private string NormalizePath( string path )
        {
            string theReturnPath = path.Replace( m_PathSeparator, m_RSPathSeparator );
            theReturnPath = Regex.Replace( theReturnPath, "/+", "/" );
            theReturnPath = string.IsNullOrEmpty( theReturnPath ) ? m_RSPathSeparator : theReturnPath;
            return theReturnPath;
        }
    }

    /// <summary>
    /// Contains the parameters for New-Item when creating a datasource
    /// </summary>
    internal class DataSourceParameters
    {
        /// <summary>
        /// Database name
        /// </summary>
        [Parameter( Mandatory = true, ValueFromPipelineByPropertyName = true )]
        public string Database { get; set; }

        /// <summary>
        /// Integrated authentication option - defaults to true
        /// </summary>
        [Parameter]
        public SwitchParameter IntegratedAuthentication { get; set; }

        /// <summary>
        /// Server name - defaults to local machine name
        /// </summary>
        [Parameter( ValueFromPipelineByPropertyName = true )]
        public string Server { get; set; }

        /// <summary>
        /// Constructor
        /// </summary>
        public DataSourceParameters()
        {
            Server = Environment.MachineName;
            IntegratedAuthentication = true;
        }
    }

    /// <summary>
    /// Contains optional parameters for New-Item when creating a report
    /// </summary>
    internal class ReportParameters
    {
        /// <summary>
        /// Full path to shared datasource
        /// </summary>
        [Parameter]
        public string DataSourcePath { get; set; }

        /// <summary>
        /// Name of shared datasource
        /// </summary>
        [Parameter]
        public string DataSourceName { get; set; }
    }

    /// <summary>
    /// Dynamic parameters for creating new resources (jpg, bmp)
    /// </summary>
    internal class ResourceParameters
    {
        /// <summary>
        /// Mime type of resource
        /// </summary>
        [Parameter( Mandatory = true )]
        public string MimeType { get; set; }
    }

    /// <summary>
    /// Dynamic parameters for creating a new drive
    /// </summary>
    internal class ReportingServicesDriveParameters
    {
        [Parameter( Mandatory = true )]
        public string WebServiceUrl { get; set; }
    }
}
